require 'rubygems'
require 'right_aws'
require 'json'

module SDB # :nodoc:all
  class Logger < ::Logger 
    LEVEL_MAPPING = {
      :debug => :trace,
      :info => :debug,
      :warn =>:info,
      :error =>:important,
      :fatal => :important
    }
    
    LEVEL_MAPPING.each do |k,v|
      define_method( k ) do |*args|
        @caplogger.send( v, *args )
      end
    end
    
    def initialize( caplogger )
      @caplogger = caplogger
    end
    
  end
end # module SDB

module Capistrano #:nodoc:
  class Configuration #:nodoc:
    module Variables #:nodoc:
      module SDB
        def self.included( base ) #:nodoc:
          %w(initialize respond_to? method_missing).each do |m|
            base_name = m[/^\w+/]
            punct     = m[/\W+$/]
            base.send :alias_method, "#{base_name}_without_sdb_variables#{punct}", m
            base.send :alias_method, m, "#{base_name}_with_sdb_variables#{punct}"
          end # each m
        end # self.included

        def initialize_with_sdb_variables(*args) #:nodoc:
          initialize_without_sdb_variables(*args)
        end # def initialize_with_sdb_variables

        def respond_to_with_sdb_variables?( sym ) #:nodoc:
          return respond_to_without_sdb_variables?( sym ) if( @_sdb_booting )
          if( sdb_local_override? )
            return true if( respond_to_without_sdb_variables?( sym ) )
            sdb_download_config
            return sdb_has_key?( sym )
          else
            sdb_download_config
            return sdb_has_key?( sym ) || respond_to_without_sdb_variables?( sym )
          end
        end # def respond_to_with_sdb_variables?

        def method_missing_with_sdb_variables( sym, *args, &block ) #:nodoc:
          return method_missing_without_sdb_variables( sym , *args, &block ) if( @_sdb_booting )
          if( args.length == 0 && block.nil? )
            if( sdb_local_override? )
              if( respond_to_without_sdb_variables?( sym ) )
                method_missing_without_sdb_variables( sym, *args, &block )
              else
                sdb_download_config
                sdb_fetch( sym )
              end
            else # if sdb_local_override?
              sdb_has_key?( sym )  ? sdb_fetch( sym ) : method_missing_without_sdb_variables( sym, *args, &block )
            end # if sdb_local_override? else
            
          else #
            method_missing_without_sdb_variables( sym, *args, &block )
          end # possible var
          
        end # def method_missing_with_sdb_variables

        # Boot the sdb system, including 
        # * creating a client
        # * creating a domain for configuration
        def require_sdb( opts = nil )
          opts ||= {}
          return if @booted
          @_sdb_booting = true
          raise "must specify aws_access_key_id and aws_secret_access_key" unless
            ( exists?( :aws_access_key_id ) && exists?( :aws_secret_access_key ) )
          @stage = respond_to?( :stage ) ? stage : 'default'
          load_precedence_setting
          raise "no stage specified" if @stage.nil?
          logger.trace "booting sdb plugin with #{@precedence} precedence"
          default_client_params = {
            :protocol => 'http',
            :port => 80,
            :logger => ::SDB::Logger.new( logger )
          }
          override_client_params = respond_to?( :sdb_client_params ) ? sdb_client_params : {}
          client_params = default_client_params.merge( override_client_params )
          self._sdb_client = RightAws::SdbInterface.new( aws_access_key_id, aws_secret_access_key, 
                                                         client_params )
                                                         
          default_domain = ENV['SDBSELF._SDB_DOMAIN'] || "#{application}_cap_config"
          self._sdb_domain = default_domain
          self._sdb_domain = sdb_domain if( exists?(:sdb_domain) )
          self._sdb_client.create_domain( self._sdb_domain )
          @_sdb_booting = false
          @booted = true
        end

        # tests whether the sdb config defines a variable or not
        # pass in :reload as the second param to force a re-download of config.
        def sdb_has_key?( variable, reload = false )
          sdb_download_config( reload )
          return @sdb_vars.has_key?( variable.to_s )
        end

        # fetches a variable from sdb.
        # pass in :reload as the second param to force a re-download of config.
        def sdb_fetch( variable, reload = false )
          logger.trace( "fetching #{variable} for #{@stage} from remote location" )
          sdb_download_config( reload )
          if( variable )
            raise IndexError, "`#{variable} not found" if( !sdb_has_key?( variable ) )
            return @sdb_vars[variable.to_s]
          else
            return @sdb_vars
          end
        end

        # takes in a hash of variables to set.
        def sdb_set( variables )
          require_sdb
          rval = self._sdb_client.put_attributes( self._sdb_domain, @stage, variables )
        end

        protected
        attr_accessor :_sdb_client, :_sdb_domain

        private

        # dump internal variable state
        def sdb_dump_vars( reload = false ) 
          sdb_download_config( reload )
          logger.debug "sdb config:\n #{@sdb_vars.inspect}"
        end


        def sdb_download_config( reload = false )
          return @sdb_vars if( !reload && @sdb_vars )
          require_sdb
          logger.trace "downloading configuration from sdb"
          # FIXME: we might still want to download data for all stages, just so 
          # that we can do a stage -> default fallback even for remote.
#           @sdb_vars = Hash[ *(self._sdb_client.query( self._sdb_domain )[:items].collect{ |item|
#                     [item, self._sdb_client.get_attributes( self._sdb_domain, @stage )[:attributes]
#                     ]}.flatten) ]
          @sdb_vars = self._sdb_client.get_attributes( self._sdb_domain, @stage )[:attributes]
        end

        def sdb_booted?
          return @booted
        end

        def load_precedence_setting
          if( @precedence.nil? )
            if( ENV['SDB_PRECEDENCE'] )
              @precedence = ENV['SDB_PRECEDENCE'].to_sym
            else
              wasbooting = @_sdb_booting
              @_sdb_booting = true
              @precedence = self.respond_to?( :sdb_precedence ) ? sdb_precedence : :local
              @_sdb_booting = wasbooting
            end
          end
        end

        def sdb_local_override?
          load_precedence_setting
          :local == @precedence
        end

        def sdb_has_stage_info?( stage )
          return @sdb_vars && @sdb_vars.has_key?( stage.to_s )
        end

      end # module SDB
    end # module Variables
  end # class Configuration
end # module Capistrano

Capistrano::Configuration.send( :include, Capistrano::Configuration::Variables::SDB )

Capistrano::Configuration.instance.load do
  namespace :sdb do 

    desc <<-DESC
    Store a variable in SDB. 
    Takes two forms
    * single key/value set
      $ cap KEY=msg VALUE=something sdb:set

    * multiple kv pairs (JSON compliant string)
      $ cap VARS='{"myarray" : [1,2,3]}'
DESC
    task :set do
      key, value = ENV['KEY'], ENV['VALUE']
      varstring = ENV['VARS']
      abort "Either specify a KEY and VALUE or a json VARS string. cap -e sdb:set for more info" if
        ( varstring.nil? && ( key.nil? || value.nil? ) )
      if( varstring )
        vars = JSON.parse( varstring )
      else
        vars = { key => value }
      end
      sdb_set( vars )
    end

    desc <<-DESC
    Dump the config stored in SDB. 
      Takes two forms:
      * single key:
           $ cap KEY=msg sdb:show
      * full dump:
           $ cap sdb:show
DESC
    task :show do
      key = ENV['KEY']
      value = sdb_fetch( key )
      if( key )
        puts "`#{key} = #{value.inspect}"
      else
        logger.important( "all keys':\n #{value.inspect}" )
      end
    end

  end # namespace sdb
end # Configuration.instance.load
